package com.wb.spring.boot.autoconfigure.core;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.Proxy;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.GZIPOutputStream;

import static com.wb.spring.boot.autoconfigure.core.ClientConstant.*;
import static java.lang.String.format;
import static org.apache.commons.lang3.StringUtils.isNotBlank;

/**
 * @author sw
 * @create 2023-03-03 11:14
 */
public interface Client {

    Response execute(Request request, Request.Options options) throws IOException;

    class Default implements Client {

        private final SSLSocketFactory sslContextFactory;
        private final HostnameVerifier hostnameVerifier;

        /**
         * Disable the request body internal buffering for {@code HttpURLConnection}.
         *
         * @see HttpURLConnection#setFixedLengthStreamingMode(int)
         * @see HttpURLConnection#setFixedLengthStreamingMode(long)
         * @see HttpURLConnection#setChunkedStreamingMode(int)
         */
        private final boolean disableRequestBuffering;

        /**
         * Create a new client, which disable request buffering by default.
         *
         * @param sslContextFactory SSLSocketFactory for secure https URL connections.
         * @param hostnameVerifier the host name verifier.
         */
        public Default(SSLSocketFactory sslContextFactory, HostnameVerifier hostnameVerifier) {
            this.sslContextFactory = sslContextFactory;
            this.hostnameVerifier = hostnameVerifier;
            this.disableRequestBuffering = true;
        }

        /**
         * Create a new client.
         *
         * @param sslContextFactory SSLSocketFactory for secure https URL connections.
         * @param hostnameVerifier the host name verifier.
         * @param disableRequestBuffering Disable the request body internal buffering for
         *        {@code HttpURLConnection}.
         */
        public Default(SSLSocketFactory sslContextFactory, HostnameVerifier hostnameVerifier,
                       boolean disableRequestBuffering) {
            super();
            this.sslContextFactory = sslContextFactory;
            this.hostnameVerifier = hostnameVerifier;
            this.disableRequestBuffering = disableRequestBuffering;
        }

        @Override
        public Response execute(Request request, Request.Options options) throws IOException {
            HttpURLConnection connection = convertAndSend(request, options);
            return convertResponse(connection, request);
        }

        Response convertResponse(HttpURLConnection connection, Request request) throws IOException {
            int status = connection.getResponseCode();
            String reason = connection.getResponseMessage();

            if (status < 0) {
                throw new IOException(format("Invalid status(%s) executing %s %s", status,
                        connection.getRequestMethod(), connection.getURL()));
            }

            Map<String, Collection<String>> headers = new LinkedHashMap<>();
            for (Map.Entry<String, List<String>> field : connection.getHeaderFields().entrySet()) {
                // response message
                if (field.getKey() != null) {
                    headers.put(field.getKey(), field.getValue());
                }
            }

            Integer length = connection.getContentLength();
            if (length == -1) {
                length = null;
            }
            InputStream stream;
            if (status >= 400) {
                stream = connection.getErrorStream();
            } else {
                stream = connection.getInputStream();
            }
            return Response.builder()
                    .status(status)
                    .reason(reason)
                    .headers(headers)
                    .request(request)
                    .body(stream, length)
                    .build();
        }

        public HttpURLConnection getConnection(final URL url) throws IOException {
            return (HttpURLConnection) url.openConnection();
        }

        HttpURLConnection convertAndSend(Request request, Request.Options options) throws IOException {
            final URL url = new URL(request.getUrl());
            final HttpURLConnection connection = this.getConnection(url);
            if (connection instanceof HttpsURLConnection) {
                HttpsURLConnection sslCon = (HttpsURLConnection) connection;
                if (sslContextFactory != null) {
                    sslCon.setSSLSocketFactory(sslContextFactory);
                }
                if (hostnameVerifier != null) {
                    sslCon.setHostnameVerifier(hostnameVerifier);
                }
            }
            connection.setConnectTimeout(options.connectTimeoutMillis());
            connection.setReadTimeout(options.readTimeoutMillis());
            connection.setAllowUserInteraction(false);
            connection.setInstanceFollowRedirects(options.isFollowRedirects());
            connection.setRequestMethod(request.getHttpMethod().name());

            Collection<String> contentEncodingValues = request.getHeaders().get(CONTENT_ENCODING);
            boolean gzipEncodedRequest =
                    contentEncodingValues != null && contentEncodingValues.contains(ENCODING_GZIP);
            boolean deflateEncodedRequest =
                    contentEncodingValues != null && contentEncodingValues.contains(ENCODING_DEFLATE);

            boolean hasAcceptHeader = false;
            Integer contentLength = null;
            for (String field : request.getHeaders().keySet()) {
                if (field.equalsIgnoreCase("Accept")) {
                    hasAcceptHeader = true;
                }
                for (String value : request.getHeaders().get(field)) {
                    if (field.equals(CONTENT_LENGTH)) {
                        if (!gzipEncodedRequest && !deflateEncodedRequest) {
                            contentLength = Integer.valueOf(value);
                            connection.addRequestProperty(field, value);
                        }
                    } else {
                        connection.addRequestProperty(field, value);
                    }
                }
            }
            // Some servers choke on the default accept string.
            if (!hasAcceptHeader) {
                connection.addRequestProperty("Accept", "*/*");
            }

            if (request.getBody() != null) {
                if (disableRequestBuffering) {
                    if (contentLength != null) {
                        connection.setFixedLengthStreamingMode(contentLength);
                    } else {
                        connection.setChunkedStreamingMode(8196);
                    }
                }
                connection.setDoOutput(true);
                OutputStream out = connection.getOutputStream();
                if (gzipEncodedRequest) {
                    out = new GZIPOutputStream(out);
                } else if (deflateEncodedRequest) {
                    out = new DeflaterOutputStream(out);
                }
                try {
                    out.write(request.getBody());
                } finally {
                    try {
                        out.close();
                    } catch (IOException suppressed) { // NOPMD
                    }
                }
            }
            return connection;
        }
    }

    /**
     * Client that supports a {@link java.net.Proxy}.
     */
    class Proxied extends Default {

        public static final String PROXY_AUTHORIZATION = "Proxy-Authorization";
        private final Proxy proxy;
        private String credentials;

        public Proxied(SSLSocketFactory sslContextFactory, HostnameVerifier hostnameVerifier,
                       Proxy proxy) {
            super(sslContextFactory, hostnameVerifier);
//            checkNotNull(proxy, "a proxy is required.");
            this.proxy = proxy;
        }

        public Proxied(SSLSocketFactory sslContextFactory, HostnameVerifier hostnameVerifier,
                       Proxy proxy, String proxyUser, String proxyPassword) {
            this(sslContextFactory, hostnameVerifier, proxy);
//            checkArgument(isNotBlank(proxyUser), "proxy user is required.");
//            checkArgument(isNotBlank(proxyPassword), "proxy password is required.");
            this.credentials = basic(proxyUser, proxyPassword);
        }

        @Override
        public HttpURLConnection getConnection(URL url) throws IOException {
            HttpURLConnection connection = (HttpURLConnection) url.openConnection(this.proxy);
            if (isNotBlank(this.credentials)) {
                connection.addRequestProperty(PROXY_AUTHORIZATION, this.credentials);
            }
            return connection;
        }

        public String getCredentials() {
            return this.credentials;
        }

        private String basic(String username, String password) {
            String token = username + ":" + password;
            byte[] bytes = token.getBytes(StandardCharsets.ISO_8859_1);
            String encoded = Base64.getEncoder().encodeToString(bytes);
            return "Basic " + encoded;
        }
    }
}

